/*
 * ShpImporter.java
 * 
 * Created by demory on Jun 15, 2009, 12:34:56 PM
 * 
 * Copyright 2008 David D. Emory
 * 
 * This file is part of Five Points. See <http://www.fpdev.org> for
 * additional information regarding the project.
 * 
 * Five Points is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Five Points is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Five Points.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.fpdev.apps.admin.shp;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import java.awt.geom.Point2D;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.ObjectOutputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import org.fpdev.core.SysProps;
import org.fpdev.core.basenet.BLink;
import org.fpdev.core.basenet.BLinkStreet;
import org.fpdev.core.basenet.BNode;
import org.fpdev.core.basenet.LinkTypes;
import org.fpdev.core.data.LegacyDB;
import org.fpdev.apps.admin.AdminClient;
import org.fpdev.apps.admin.ImportGraph;
import org.fpdev.apps.admin.actions.CreateLinkAction;
import org.fpdev.util.FPUtil;
import org.geotools.data.FeatureSource;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.jgrapht.graph.DefaultEdge;
import org.jgrapht.graph.SimpleGraph;
import org.jgrapht.traverse.ClosestFirstIterator;
import org.opengis.feature.simple.SimpleFeature;

/**
 *
 * @author demory
 */
public class ShpImporter {

  private AdminClient ac_;
  private SortedMap<String, ShpImportProfile> profiles_;

  public ShpImporter(AdminClient ac) {
    ac_ = ac;
    profiles_ = new TreeMap<String, ShpImportProfile>();
  }
  
  public Iterator<ShpImportProfile> getProfiles() {
    return profiles_.values().iterator();
  }

  public ShpImportProfile getProfile(String name) {
    return profiles_.get(name);
  }

  public boolean checkProfileName(String name) {
    return !profiles_.containsKey(name);
  }

  public void addProfile(ShpImportProfile profile, boolean writeFile) {
    profiles_.put(profile.getName(), profile);
    if(writeFile) writeProfiles();
  }

  public void deleteProfile(String name) {
    profiles_.remove(name);
    writeProfiles();
  }

  public void writeProfiles() {
    System.out.println("writing file w/ "+profiles_.size()+" profiles");

    String filename = ac_.getEngine().getProperty(SysProps.FP_HOME)+"conf"+File.separator+"shpprofiles.xml";
    try {
      System.out.println("  writing file: "+filename);
      File file = new File(filename);
      if(!file.exists()) file.createNewFile();
      FileWriter writer = new FileWriter(file);

      writer.write("<?xml version=\"1.0\"?>\n");
      writer.write("<shpprofiles>\n");
      for (Iterator<ShpImportProfile> it = getProfiles(); it.hasNext();)
        writer.write(it.next().getXml());
      writer.write("</shpprofiles>\n");
      writer.close();

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public void importFromShapefile(ShpImportProfile profile, URL shapeURL) {
    try {
      // get feature results
      ShapefileDataStore store = new ShapefileDataStore(shapeURL);
      // feature type name is defaulted to the name of shapefile (without extension)
      String name = store.getTypeNames()[0];
      FeatureSource source = store.getFeatureSource(name);
      FeatureCollection fColl = source.getFeatures();

      System.out.println("Feature collection type: " + fColl.getSchema().getName());
      System.out.println("Num features: " + fColl.size());

      //AttributeType[] attrs = store.getSchema().getAttributeTypes();

      FeatureIterator fIter = fColl.features();
      List<BLink> newLinks = new LinkedList<BLink>();
      int c = 0;
      while (fIter.hasNext()) {
        c++;
        if (c % 100 == 0) {
          System.out.println("loaded " + c + " features");
        }

        SimpleFeature f = (SimpleFeature) fIter.next();

        // initialize link ID
        int linkID = 0;
        if(!profile.getAutoAssignLinkIDs()) { // read link id from attribute
          linkID = ((Integer) f.getAttribute(profile.getLinkIDAttrName())).intValue();
          if (ac_.getEngine().getBaseNet().linkExists(linkID)) {
            ac_.msg("Link already exists w/ id=" + linkID);
            continue;
          }
        }
        else { // auto assign link id
          // TODO: implement
          throw new UnsupportedOperationException("Not yet implemented");
        }

        // initialize from/to node IDs
        int fNodeID = 0, tNodeID = 0;
        if(!profile.getAutoAssignNodeIDs()) { // read node id from attribute
          fNodeID = ((Integer) f.getAttribute(profile.getFNodeIDAttrName())).intValue();
          tNodeID = ((Integer) f.getAttribute(profile.getTNodeIDAttrName())).intValue();
        }
        else {
          throw new UnsupportedOperationException("Not yet implemented");
        }

        // init other fields
        int fAddrL = ((Integer) f.getAttribute(profile.getAttributeNameForRole(ShpImportProfile.AttributeRole.FADDRL))).intValue();
        int tAddrL = ((Integer) f.getAttribute(profile.getAttributeNameForRole(ShpImportProfile.AttributeRole.TADDRL))).intValue();
        int fAddrR = ((Integer) f.getAttribute(profile.getAttributeNameForRole(ShpImportProfile.AttributeRole.FADDRR))).intValue();
        int tAddrR = ((Integer) f.getAttribute(profile.getAttributeNameForRole(ShpImportProfile.AttributeRole.TADDRR))).intValue();
        int zipL = ((Integer) f.getAttribute(profile.getAttributeNameForRole(ShpImportProfile.AttributeRole.ZIPL))).intValue();
        int zipR = ((Integer) f.getAttribute(profile.getAttributeNameForRole(ShpImportProfile.AttributeRole.ZIPR))).intValue();
        String linkname = (String) f.getAttribute(profile.getAttributeNameForRole(ShpImportProfile.AttributeRole.LINK_NAME));

        Geometry g = (Geometry) f.getDefaultGeometry();
        Coordinate[] coords = g.getCoordinates();

        BNode fNode;
        if (ac_.getEngine().getBaseNet().nodeExists(fNodeID)) {
          fNode = ac_.getEngine().getNodeFromID(fNodeID);
        } else {
          fNode = ac_.getNetworkOps().newNode(fNodeID, coords[0].x, coords[0].y, true).getNode();
        }
        BNode tNode;
        if (ac_.getEngine().getBaseNet().nodeExists(tNodeID)) {
          tNode = ac_.getEngine().getNodeFromID(tNodeID);
        } else {
          tNode = ac_.getNetworkOps().newNode(tNodeID, coords[coords.length - 1].x, coords[coords.length - 1].y, true).getNode();
        }

        BLink link = null;
        if (coords.length > 2) {
          List<Point2D.Double> shpPts = new LinkedList<Point2D.Double>();
          for (int i = 1; i < coords.length - 1; i++) {
            shpPts.add(new Point2D.Double(coords[i].x, coords[i].y));
          }
          CreateLinkAction cla = ac_.getNetworkOps().newLink(linkID, null, fNode, tNode, LinkTypes.LOCAL_TWOWAY, shpPts, 0, false);
          if (cla != null) {
            link = cla.getLink();
          }
        } else {
          CreateLinkAction cla = ac_.getNetworkOps().newLink(linkID, null, fNode, tNode, LinkTypes.LOCAL_TWOWAY, null, 0, false);
          if (cla != null) {
            link = cla.getLink();
          }
        }
        if (link != null) {
          link.setName(linkname);
          ((BLinkStreet) link).setAddressRange(fAddrL, fAddrR, tAddrL, tAddrR);
          ((BLinkStreet) link).setZips(zipL, zipR);
          newLinks.add(link);
        }
      }
      System.out.println("loaded " + c + " features total");

      LegacyDB db = ac_.getEngine().getDB();

      // write links to db
      System.out.print("Writing links to db...");
      Iterator<BLink> links = newLinks.iterator();
      db.setActiveTable("links");
      while (links.hasNext()) {
        db.initLink(links.next());
      }
      db.flushInsertOps();
      System.out.println("done");

      // write addr ranges
      System.out.print("Writing addrs to db...");
      links = newLinks.iterator();
      db.setActiveTable("addrranges");
      while (links.hasNext()) {
        db.initAddrRange((BLinkStreet) links.next());
      }
      db.flushInsertOps();
      System.out.println("done");

      // write shapepoints
      System.out.print("Writing shppts to db...");
      links = newLinks.iterator();
      db.setActiveBlobTable("shapepoints");
      while (links.hasNext()) {
        BLink link = links.next();
        ByteArrayOutputStream byteOS = new ByteArrayOutputStream();
        ObjectOutputStream objOS = new ObjectOutputStream(byteOS);
        Iterator<Point2D.Double> shpPts = link.getShapePoints();
        int sp = 0;
        while (shpPts.hasNext()) {
          Point2D.Double pt = shpPts.next();
          objOS.writeDouble(pt.x);
          objOS.writeDouble(pt.y);
          sp++;
        }
        objOS.close();
        if (sp > 0) {
          db.initSPBlob(link.getID(), byteOS.toByteArray());
        }
      }
      db.flushBlobInsertOps();
      System.out.println("done");

      ac_.getGUI().getMapPanel().refresh(false, true, false);

    } catch (Exception e) {
      System.out.println("Exception in importFromShapefile()");
      e.printStackTrace();
    }
  }

  public void importFromShapefile2(ShpImportProfile profile, URL shapeURL) {

    ac_.getNetworkOps().setVerbose(false);

    try {

      // get feature results
      ShapefileDataStore store = new ShapefileDataStore(shapeURL);
      // feature type name is defaulted to the name of shapefile (without extension)
      String name = store.getTypeNames()[0];
      FeatureSource source = store.getFeatureSource(name);
      FeatureCollection fColl = source.getFeatures();

      long totalItems = fColl.size();
      ac_.msg("Feature collection type: " + fColl.getSchema().getName());
      ac_.msg("Number of features: " + totalItems);

      // READ FEATURES INTO IMPORT GRAPH
      FeatureIterator fIter = fColl.features();

      ac_.getGUI().statusText("Importing features to graph...");
      ImportGraph iLinks = new ImportGraph();
      long numProcessed = 0;

      while (fIter.hasNext()) {

        SimpleFeature f = (SimpleFeature) fIter.next();
        Geometry g = (Geometry) f.getDefaultGeometry();
        Coordinate[] coords = g.getCoordinates();
        iLinks.addLink(new ImportGraph.Link(coords, f));
        numProcessed++;
        double pctWrittenD = 100.0 * (double) numProcessed / (double) totalItems;
        ac_.setProgress((int) pctWrittenD);

      }
      ac_.setProgress(0);

      //cv_.getGUI().getMapPanel().getDrawItems().addItem("impshp", iLinks, "m");
      ac_.getGUI().getMapPanel().refresh(false, true, false);

      // check node concurrency
      double tol = 10;
      SimpleGraph<ImportGraph.Node, DefaultEdge> proxGraph =
              new SimpleGraph<ImportGraph.Node, DefaultEdge>(DefaultEdge.class);
      Map<Coordinate, BNode> coordLookup = new HashMap<Coordinate, BNode>();

      List<Coordinate> endPts = iLinks.endPoints();
      int epCount = endPts.size();
      ImportGraph.Node iNodes[] = new ImportGraph.Node[epCount];

      ac_.getGUI().statusText("Initializing proximity graph vertices...");
      totalItems = epCount;
      Iterator<Coordinate> epIter = endPts.iterator();
      int i1 = 0;
      while (epIter.hasNext()) {
        Coordinate c1 = epIter.next();
        ImportGraph.Node iNode = new ImportGraph.Node("" + i1, c1);
        proxGraph.addVertex(iNode);
        iNodes[i1] = iNode;
        i1++;
        double pctWrittenD = 100.0 * (double) i1 / (double) totalItems;
        ac_.setProgress((int) pctWrittenD);
      }
      ac_.setProgress(0);


      ac_.getGUI().statusText("Computing proximity graph distances...");
      System.out.println("epCount="+epCount);
      totalItems = epCount*epCount/2;
      System.out.println("totalItems="+totalItems);
      numProcessed = 0;
      Coordinate[] coords = new Coordinate[endPts.size()];
      int i = 0;
      for(epIter = endPts.iterator(); epIter.hasNext();) coords[i++] = epIter.next();
      for (i1 = 0; i1 < coords.length; i1++) {
        for (int i2 = i1+1; i2 < coords.length; i2++) {
          double dist = FPUtil.magnitude(coords[i1].x, coords[i1].y, coords[i2].x, coords[i2].y);
          if (dist <= tol) { // create link
            proxGraph.addEdge(iNodes[i1], iNodes[i2]);
          }
          numProcessed++;
          if(numProcessed % 1000000 == 0) {
            System.out.println("numP="+numProcessed);
            double pctWrittenD = 100.0 * (double) numProcessed / (double) totalItems;
            ac_.setProgress((int) pctWrittenD);
          }
        }
      }
      ac_.setProgress(0);

      System.gc();

      ac_.getGUI().statusText("Reducing proximity graph and creating nodes...");
      Set<ImportGraph.Node> verts = new HashSet<ImportGraph.Node>();
      verts.addAll(proxGraph.vertexSet());
      totalItems = verts.size();
      //engine_.getDB().setActiveTable("nodes");
      while (!verts.isEmpty()) {//vIter.hasNext()) {
        ImportGraph.Node iNode = verts.iterator().next(); //vIter.next();
        if (proxGraph.degreeOf(iNode) == 0) { // freestanding node
          //System.out.println("fs node "+iNode.getID());
          verts.remove(iNode);
          BNode node = ac_.getNetworkOps().newNode(iNode.getCoord().x, iNode.getCoord().y, false).getNode();
          //engine_.getDB().initNode(node);
          coordLookup.put(iNode.getCoord(), node);
        } else {
          double tx = 0, ty = 0, count = 1;
          //System.out.println("grouped node "+iNode.getID());
          tx += iNode.getCoord().x;
          ty += iNode.getCoord().y;
          ClosestFirstIterator cfi = new ClosestFirstIterator(proxGraph, iNode);
          while (cfi.hasNext()) {
            ImportGraph.Node in = (ImportGraph.Node) cfi.next();
            //System.out.println(" in: "+in.getID());
            verts.remove(in);
            tx += in.getCoord().x;
            ty += in.getCoord().y;
            count++;
          }
          BNode node = ac_.getNetworkOps().newNode(tx / count, ty / count, false).getNode();
          //engine_.getDB().initNode(node);

          // re-iterate, updating coord lookup table
          cfi = new ClosestFirstIterator(proxGraph, iNode);
          while (cfi.hasNext()) {
            coordLookup.put(((ImportGraph.Node) cfi.next()).getCoord(), node);
          }

        }
        double pctWrittenD = 100.0 * (double) (totalItems - verts.size()) / (double) totalItems;
        ac_.setProgress((int) pctWrittenD);

      }
      //engine_.getDB().flushInsertOps();
      ac_.setProgress(0);

      System.gc();

      // INITIALIZE LINKS
      ac_.getGUI().statusText("Creating links...");
      totalItems = iLinks.linkCount();
      numProcessed = 0;
      //engine_.getDB().setActiveTable("links");
      Iterator<ImportGraph.Link> links = iLinks.links();
      while (links.hasNext()) {
        ImportGraph.Link igLink = links.next();
        SimpleFeature f = igLink.getFeature();
        //System.out.println(link);
        int linkType = profile.resolveFacilityType(f);
        CreateLinkAction cla = ac_.getNetworkOps().newLink(null, coordLookup.get(igLink.start()), coordLookup.get(igLink.end()), linkType, igLink.shapePoints(), 0, false);
        if (cla != null && cla.getLink() != null) {
          //engine_.getDB().initLink(cla.getLink());
          //if(divideLinks) processedLinks.add(cla.getLink());

          BLink bLink = cla.getLink();

          String linkname = (String) f.getAttribute(profile.getAttributeNameForRole(ShpImportProfile.AttributeRole.LINK_NAME));
          bLink.setName(linkname);

          if(bLink.getClassType() == BLink.CLASS_STREET) {

            int fAddrL, fAddrR, tAddrL, tAddrR, zipL, zipR;

            try { fAddrL = Integer.parseInt(f.getAttribute(profile.getAttributeNameForRole(ShpImportProfile.AttributeRole.FADDRL)).toString()); }
            catch(NumberFormatException ex) { fAddrL = 0; }
            try { fAddrR = Integer.parseInt(f.getAttribute(profile.getAttributeNameForRole(ShpImportProfile.AttributeRole.FADDRR)).toString()); }
            catch(NumberFormatException ex) { fAddrR = 0; }
            try { tAddrL = Integer.parseInt(f.getAttribute(profile.getAttributeNameForRole(ShpImportProfile.AttributeRole.TADDRL)).toString()); }
            catch(NumberFormatException ex) { tAddrL = 0; }
            try { tAddrR = Integer.parseInt(f.getAttribute(profile.getAttributeNameForRole(ShpImportProfile.AttributeRole.TADDRR)).toString()); }
            catch(NumberFormatException ex) { tAddrR = 0; }
            try { zipL = Integer.parseInt(f.getAttribute(profile.getAttributeNameForRole(ShpImportProfile.AttributeRole.ZIPL)).toString()); }
            catch(NumberFormatException ex) { zipL = 0; }
            try { zipR = Integer.parseInt(f.getAttribute(profile.getAttributeNameForRole(ShpImportProfile.AttributeRole.ZIPR)).toString()); }
            catch(NumberFormatException ex) { zipR = 0; }

            ((BLinkStreet) bLink).setAddressRange(fAddrL, fAddrR, tAddrL, tAddrR);
            ((BLinkStreet) bLink).setZips(zipL, zipR);
          }
          
        }
        numProcessed++;
        double pctWrittenD = 100.0 * (double) numProcessed / (double) totalItems;
        ac_.setProgress((int) pctWrittenD);

      }
      //engine_.getDB().flushInsertOps();
      ac_.setProgress(0);
      ac_.getGUI().statusText("");

      ac_.getGUI().getMapPanel().refresh(false, true, false);


    } catch (Exception e) {
      System.out.println("Exception in importShapefile()");
      e.printStackTrace();
    }

    //if(divideLinks) divideLinksByPoints(processedLinks);
    //verbose_ = true;
    ac_.getNetworkOps().setVerbose(true);

  }

  public static class FacilityTypeRule {
    private String attrName_, attrVal_;
    private int facType_;

    public FacilityTypeRule(String attrName, String attrVal, int facType) {
      attrName_ = attrName;
      attrVal_ = attrVal;
      facType_ = facType;
    }

    public String getAttrName() { return attrName_; }

    public String getAttrValue() { return attrVal_; }

    public int getFacilityType() { return facType_; }
  }
}
